classdef injoinQuery < query
  % Class for inner join (multiple table) SQL Server queries
  % Derived from query
  % C-tor sig: jObject = injoinQuery(conn, parameters); 
  %     for details, type help('injoinQuery.injoinQuery')
  %
  % Author: David Goldsmith, Wash. State Dept. of Ecology, dgol461@ecy.wa.gov
  % Release date: 9/15/2011
  
    properties
      % Inherited from SQLstatement
%         con; cat; sch; meta;
%         tbls = {};
%         aCc = {}; % cell array of available columns, w/out info as to their source table(s)
%         pkey = struct('key', '', 'val', []);
%         result = {};
%         issuedStatementError = false;
%         issuedAvailableError = false;
%         errCode = {};
%         duration;
%     (Dependent = true)
%         DBname;
%         statement;
%         availableTables;
%         availableColumns;
      % Inherited from query
%         cols = {};
%         orderBy = {};
%         where   = '';
%         CSD = struct(...% CSD = Common Select "Decorators"
%               'colEquivalents', {{'*', 'GETDATE()', 'GETUTCDATE()'}},...
%               'globalModifiers', {{'ALL', 'DISTINCT', 'TOP \d+'}},...
%               'singleColFuncs', {{'AVG', 'COUNT', 'MAX', 'MEDIAN', 'MIN', 'STDDEV', 'SUM', 'VARIANCE'}},...
%               'dateFuncs', {{'DATEADD', 'DATEDIFF'}},...
%               'dateParts', {{'year', 'yy', 'yyyy', 'quarter', 'qq', 'q', 'month', 'mm', 'm',...
%                              'dayofyear', 'dy', 'y', 'day', 'dd', 'd', 'week', 'wk', 'ww', 'hour',...
%                              'hh', 'minute', 'mi', 'n', 'second', 'ss', 's', 'millisecond', 'ms'}},...
%               'as', {{'AS'}});
%         allCSD = {};
        on = {};
    end
    
    properties (Dependent = true, Access = private)
        selectClause;
        fromClause;
        orderbyClause;
    end
    
    methods % Accessible, i.e., non-private
      % Inherited from SQLstatement
%         SQLstatement(DBname, un, pwd, devVerOverride) 
%         assembleStatement(obj) (Abstract)
%         delete(obj)             
%         procErr(obj)
%         setPropVals(obj, prop, elements, extend) (Abstract) 
%         submitStatement(obj) (Abstract) 
%
      % Inherited from query
%         query(DBname, un, pword, parameters, devVerOverride)
%         isCSD(obj, arg)
%         processTblsChange(obj, oldValidCols) (overridden)
%         validateInput(obj, prop, elements, extend) (overridden)

        function jObject = injoinQuery(conn, parameters)
          % The join query object's constructor
          %
          % Arguments: (req = required, opt = optional, ca = cell array)
          %
          %   except as noted below, same as query
          %
          %   in parameters, only tbls is changed, and "on" is new:
          %
          %      + tbls - ca of ca of strings, opt; the tables to join
          %
          %      + on   - ca of ca of strings, opt; the columns on which to join the tables
          %
          %      Ex.: Suppose parameters.tbls = {{'RawValue', 'CalculatedValue', 'RealTimeFlags'}}
          %           and parameters.on = {{'RawValueID', 'RealTimeFlagsID'}}.  These inputs will
          %           result in the following FROM clause:
          %
          % FROM RawValue INNER JOIN CalculatedValue ON RawValue.RawValueID=CalculatedValue.RawValueID
          %   INNER JOIN RealTimeFlags ON RawValue.RealTimeFlagsID=RealTimeFlags.RealTimeFlagsID
          %
          %          (Note that in the DB from which these values were taken (Mooring_COTS_Dev) the 
          %           CalculatedValue table has no RealTimeFlagsID column, and the RealTimeFlags 
          %           table has no RawValueID column, and yet this is a valid multiple-table inner 
          %           join, constructible automatically by this class' get.onClause method)

            if nargin < 1, conn = ''; end
            if nargin < 2, parameters = struct.empty; end
            
            jObject = jObject@query(conn, struct.empty);
            paraFields = fieldnames(parameters);
            for prop = {'tbls', 'cols', 'orderBy', 'where', 'on'}
                prop = prop{:};
                if ismember(prop, paraFields)
                    jObject.setPropVals(prop, parameters.(prop));
                end
            end
            
        end
        
        function validateInput(obj, prop, elements, extend)
            
            global lfcr

            switch prop
                
                case 'where'
                    if ~(isempty(elements) || ischar(elements) || iscellstr(elements))
                        obj.errCode{end+1} = ['injoinQuery.setPropVals(where) error: wrong type of input for '...
                                       '"where" property'];
                    end
                    
                case 'tbls'
                    if numel(elements)==1 && ~extend
                        obj.errCode{end+1} = ['injoinQuery.setPropVals(tbls) error: asked to make tbls '...
                                       'contain only one element; this is not allowed in a "join" '...
                                       'query, please use the "query" class.'];
                    elseif ~iscellstr(elements)
                        obj.errCode{end+1} = ['injoinQuery.setPropVals(tbls) error: wrong type input for '...
                                       '"tbls" porperty (not the required cell array of strings)'];
                    elseif numel(elements) ~= numel(intersect(elements, obj.availableTables))
                        obj.errCode{end+1} = 'injoinQuery.setPropVals(tbls) error: invalid table requested';
                    end
                    
                case 'orderBy'
                    if any(~(cellfun(@iscellstr, elements(:)) & cellfun(@numel, elements(:))==2)) 
                        obj.errCode{end+1} = ['injoinQuery.setPropVals(orderBy) error: at least one '...
                                       'input value for the "orderBy" property' lfcr 'is not a '...
                                       'length-2 cell array of strings'];
                    else
                        elements = [elements{:}];
                        orderSpecs = elements(2*(1:(numel(elements)/2))); 
                        if any(~ismember(orderSpecs, {'asc', 'des'}))
                            obj.errCode{end+1} = ['injoinQuery.setPropVals(orderBy) error: at least '...
                                                  'one order specifier is neither ''asc''' lfcr...
                                                  'nor ''des''.  (Sorry, case must match.)'];
                        end
                        obj.confirmColAvail(elements(2*(1:(numel(elements)/2))-1), prop);
                    end
                    
                case 'on'
                    if ~(isempty(elements) || iscellstr(elements))
                        obj.errCode{end+1} = ['injoinQuery.setPropVals(on) error: wrong input type '...
                                       'for "on" property' lfcr '(must be either empty or a '...
                                       'cell array of strings'];
                    else
                        obj.confirmColAvail(elements, 'on');
                    end

                case 'cols'
                    if ~(isempty(elements) || iscellstr(elements))
                        obj.errCode{end+1} = 'injoinQuery.setPropVals(cols) error: wrong input type';
                    else
                        obj.confirmColAvail(elements, prop);
                    end
                    
                otherwise
                   obj.errCode{end+1} = ['injoinQuery.setPropVals error: invalid prop ' prop];
                    
            end
            
        end
        
        function confirmColAvail(obj, cols, prop)
            
            global lfcr
            
            if isempty(obj.tbls)
                obj.errCode{end+1} = ['injoinQuery.setPropVals(' prop ') error: tbls property unset, can''t '...
                               'assure availability' lfcr 'of any columns, so not setting '...
                               'property ' prop];
            else           
                aC = obj.availableColumns;
                switch prop
                    
                    case 'orderBy'
                        if ~isempty(setdiff(cols, obj.aCc))
                            obj.errCode{end+1} = ['injoinQuery.setPropVals(orderBy) error: given the '...
                               'current table list,' lfcr 'at least one column requested for the '...
                               'ORDER BY clause is not presently available' lfcr '(or may not '...
                               'exist at all in the current DB).'];
                        end
                        
                    case 'cols'
                      % Note: this check doesn't guarantee SELECT clause syntactical correctness,
                      % but it is intended to get pretty close
                        
                        nc = numel(cols);
                        cEs = union(obj.aCc, obj.CSD.colEquivalents); % column "equivalents" includes
                                                                      % columns, of course

                        gMs = cellstrIsMember(cols, obj.CSD.globalModifiers);
                        allDisPos = cellstrIsMember(cols, {'ALL', 'DISTINCT'});
                        topPos = gMs - allDisPos;
                        if gMs(end) || (sum(abs(diff(gMs))) > 1)
                            obj.errCode{end+1} = ['injoinQuery.setPropVals(cols) syntax error: one '...
                                                  'or more "global modifiers," ' lfcr sprintf(... 
                                                  '%s, ', obj.CSD.globalModifiers{1:end-1})...
                                                  obj.CSD.globalModifiers{end}(1:end-3) '<integer> '...
                                                  'follows something other than a global modifier, '...
                                                  lfcr 'or you''ve provided only global modifiers '...
                                                  'for the cols property.'];
                        elseif find(topPos, 1) < find(allDisPos, 1)
                            obj.errCode{end+1} = ['injoinQuery.setPropVals(cols) syntax error: a '...
                                                  '"TOP" in your requested SELECT clause' lfcr...
                                                  'precedes an "ALL" or "DISTINCT"; SQL Server '...
                                                  'considers this a syntax error.'];
                        end
                        
                        sMs = find(cellstrIsMember(cols, obj.CSD.singleColFuncs));
                        if ismember(nc, sMs) || ... % last element can't be a single column function
                           any(~cellstrIsMember(cols(sMs+1), cEs)) % a col. equiv. must follow a s.c.f.
                            obj.errCode{end+1} = ['injoinQuery.setPropVals(cols) syntax error: one '...
                                                  'or more "single-column functions," ' lfcr... 
                                                  sprintf('%s, ', obj.CSD.singleColFuncs{:}) lfcr...
                                                  'present in your requested SELECT clause is not '...
                                                  'immediately followed by an available' lfcr...
                                                  'column or "column equivalent" (' sprintf('%s, ',...
                                                  obj.CSD.colEquivalents{1:end-1})...
                                                  obj.CSD.colEquivalents{end} ').'];
                        end
                        
                        mMs = find(cellstrIsMember(cols, obj.CSD.dateFuncs));
                        prefix = 'injoinQuery.setPropVals(cols) syntax error: ';
                        if ~isempty(mMs) && nc > 3
                            if any(ismember(nc-2:nc, mMs))
                                obj.errCode{end+1} = [prefix 'the datetime functions, ' sprintf('%s, ',...
                                                      obj.CSD.dateFuncs{:}) lfcr 'may not be '...
                                                      'among the last three elements in a SELECT clause.']; 
                            else

                                if any(~cellstrIsMember(cols(mMs+1), obj.CSD.dateParts))
                                    obj.errCode{end+1} = [prefix 'the datetime functions, ' sprintf(...
                                                          '%s, ', obj.CSD.dateFuncs{:}) lfcr...
                                                          'must be immediately followed by one of '...
                                                          sprintf('%s, ', obj.CSD.dateParts{1:12}) lfcr...
                                                          sprintf('%s, ', obj.CSD.dateParts{13:end-1})...
                                                          sprintf('%s.', obj.CSD.dateParts{end})]; 
                                end

                                for indx = mMs
                                    func = cols{indx};
                                    scndArg = cols{indx+2};
                                    isScndArgCol = ismember(scndArg, obj.aCc); % exclude col equivalents
                                    tmp = extract_leading_number(scndArg);
                                    isNum = isnumeric(tmp) && ~isempty(tmp);
                                    switch func
                                        case 'DATEADD' % second argument must be an int type
                                            if isScndArgCol
                                                scndArgTyp = getColumnType(scndArg, obj.tbls{:}, obj.con);
                                                if ~iscell(scndArgTyp)
                                                    scndArgTyp = {scndArgTyp};
                                                end
                                                if all(cellfun(@isempty, regexpi(scndArgTyp, 'int')))
                                                    obj.errCode{end+1} = [prefix 'Second argument of DATEADD '...
                                                        scndArg ' not the required (i.e., integer) type.'];
                                                end
                                                for sAT=scndArgTyp'
                                                    sAT = sAT{:};
                                                    if any(~cellfun(@isempty, regexp(sAT, ...
                                                           {'Attempt', 'Query', 'Sorry'})))
                                                        obj.errCode{end+1} = sAT;
                                                        break
                                                    end
                                                end
                                            elseif isNum
                                                nScndArg = str2double(scndArg);
                                                if numel(int64(nScndArg)) > 1 || int64(nScndArg) ~= nScndArg
                                                    obj.errCode{end+1} = [prefix 'Second argument of DATEADD '...
                                                        scndArg ' not the required (i.e., integer) type.'];
                                                end
                                            end
                                            
                                        case 'DATEDIFF' % second argument must be a datetime type
                                            if isScndArgCol
                                                scndArgTyp = getColumnType(scndArg, obj.tbls{:}, obj.con);
                                                if ~iscell(scndArgTyp)
                                                    scndArgTyp = {scndArgTyp};
                                                end
                                                for sAT=scndArgTyp'
                                                    sAT = sAT{:};
                                                    if all(cellfun(@isempty, regexpi(sAT, ...
                                                            {'date', 'time'})))
                                                        obj.errCode{end+1} = [prefix 'Second argument of DATEDIFF '...
                                                            scndArg ' not the required (i.e., datetime) type.'];
                                                        break
                                                    elseif any(~cellfun(@isempty, regexp(sAT, ...
                                                                {'Attempt', 'Query', 'Sorry'})))
                                                        obj.errCode{end+1} = sAT;
                                                        break
                                                    end
                                                end
                                            else
                                                if ~isDateTime(scndArg)
                                                    obj.errCode{end+1} = [prefix 'Couldn''t convert '...
                                                        'second argument of DATEDIFF ' scndArg ' to '...
                                                        'the required (i.e., datetime) type.'];
                                                end
                                            end  
                                    end
                                end

                                thrdArg = cols(indx+3);
                                for col = thrdArg'
                                    col = col{:};
                                    if ismember(col, obj.aCc) % if col is an available column in the table
                                        thrdArgTyp = getColumnType(col, obj.tbls{:}, obj.con);
                                        if any(~cellfun(@isempty, regexp(thrdArgTyp, {'Attempt', ...
                                                'Query', 'Sorry'})))
                                            obj.errCode{end+1} = thrdArgTyp;
                                        elseif all(cellfun(@isempty, regexpi(thrdArgTyp, {'date', 'time'})))
                                            obj.errCode{end+1} = [prefix 'the datetime functions, '...
                                                                  sprintf('%s, ', obj.CSD.dateFuncs{:})...
                                                                  lfcr 'require a datetime type '...
                                                                  'for their third argument.'];
                                        end
                                    elseif ~isDateTime(col)
                                        obj.errCode{end+1} = [prefix 'Couldn''t convert third argument '...
                                            'of datetime function ' cols{indx} lfcr 'to the '...
                                            'required (i.e., datetime) type.'];
                                    end
                                end
                            end
                            
                        elseif ~isempty(mMs) % && nc <= 3 implies a datetime function was found, 
                                             % but there are fewer than the required three arguments
                            obj.errCode{end+1} = [prefix 'Not enough arguments for your datetime '...
                                                  'function(s).  (Exactly three are required.)'];
                        end
                        
                        as = find(cellstrIsMember(cols, obj.CSD.as));
                        if ~isempty(as)
                            if any([1 nc]==as) % AS can't be first or last
                                obj.errCode{end+1} = ['injoinQuery.setPropVals(cols) syntax error: '...
                                                      'AS can be neither the first nor the last '...
                                                      'element' lfcr 'in a SELECT clause.'];
                            elseif any(~cellstrIsMember(cols(as-1), cEs)) % AS must be preceded by a c.e.
                              % AS must also be followed by a string literal, but this is already garanteed
                                obj.errCode{end+1} = ['injoinQuery.setPropVals(cols) syntax error: '...
                                                      'AS must be immediately preceded by a' lfcr...
                                                      '"column equivalent."'];
                            end
                        end
                        
                    case 'on'
                        nc = numel(cols); nt = numel(obj.tbls);
                        if ~(nc==1 || nc==(nt-1))
                            if nt==2
                                obj.errCode{end+1} = ['injoinQuery.setPropVals(on) error: wrong '...
                                               'length input for "on" property' lfcr '(length of '...
                                               'cellstring array must be 1 if joining only two tables).'];
                            else
                                obj.errCode{end+1} = ['injoinQuery.setPropVals(on) error: wrong '...
                                               'length input for "on" property' lfcr '(length of '...
                                               'cellstring array must be either 1 or ' num2str(nt-1)...
                                               ', i.e., length(tbls)-1).'];
                            end
                            return;
                        end
                        
                        if nc==1 & nt > 2 % if there are more than two tables but only one col.
                                          % change cols into a cellstr array of size nt-1 filled
                                          % with copies of cols
                            tmp = cell(nt-1,1);
                            tmp(:) = deal(cols(:));
                            cols = tmp;
                            nc = numel(cols);
                        end
                        columnsSoFar = aC.(obj.tbls{1});
                        for i=1:nc
                            nextTable = obj.tbls{i+1};
                            if ~ismember(cols{i}, intersect(aC.(nextTable), columnsSoFar))
                                obj.errCode{end+1} = ['injoinQuery.setPropVals(on) error: given the '...
                                   'current table list (and its order),' lfcr 'at least one '...
                                   'column requested for the "on" clause is either out of order '...
                                   'or not' lfcr 'available in one or both of its positionally-'...
                                   'corresponding table in the table list' lfcr '(property '...
                                   '"tbls") and (at least) one of the tables that precedes that '...
                                   'table in the' lfcr 'list.'];
                                break;
                            else
                                columnsSoFar = union(columnsSoFar, aC.(nextTable));
                            end
                        end
                                                                
                end
            end
        end
        
        function setPropVals(obj, prop, elements, extend)
          % Sets property values, enforcing content restrictions => use instead of direct assignment
          % extend is an optional logical indicating whether to replace (false, the default) or 
          % supplement (true) obj.(prop) with elements
            if nargin < 4
                extend = false;
            end

            if ~(isequal(prop, 'where') || iscell(elements))
                elements = {elements};
            end
            
          % Refresh error code container
            obj.errCode = {};
            
          % Validate inputs
            obj.validateInput(prop, elements, extend);
            
            if isempty(obj.errCode)
              % If changing the table, save the current list of valid columns
              % for later use checking the where clause
                if strcmp(prop, 'tbls') && ~isempty(obj.tbls)
                    obj.availableColumns; % make sure obj.aCc is up-to-date
                    oldValidCols = obj.aCc;
                end
                
              % The "Action"
                if extend 
                    obj.(prop) = extendArray(obj.(prop), elements);
                else
                    obj.(prop) = elements;
                end
                
                if exist('oldValidCols', 'var')
                    obj.processTblsChange(oldValidCols);
                end
            end
        end
        
        function processTblsChange(obj, oldValidCols)
          % If changing the table: cols, orderBy, and where may need modification (and, in general,
          % where can't be "corrected" automatically, so will just be emptied if necessary)
            aC = obj.availableColumns;
          % Keep those cols which are still available
            tmpc = {};
            for i=1:numel(obj.cols)
                if ismember(obj.cols{i}, obj.aCc) || ~isempty(strfind(obj.cols{i}, '*'))
                    tmpc{end+1} = obj.cols{i};
                end
            end
            if ~isequal(obj.cols, tmpc)
                obj.cols = tmpc;
                obj.errCode{end+1} = ['injoinQuery.setPropVals ALERT: table change required change in '...
                               'cols property'];
            end
          % Do the same for orderBy
            tmpoB = {};
            for i=1:numel(obj.orderBy)
                if ismember(obj.orderBy{i}{1}, obj.aCc) 
                    tmpoB{end+1} = obj.orderBy{i};
                end
            end
            if ~isequal(flatten(obj.orderBy), flatten(tmpoB))
                obj.orderBy = tmpoB;
                obj.errCode{end+1} = ['injoinQuery.setPropVals ALERT: table change required change in '...
                               'orderBy property'];
            end
          % Keep those columns in ON which are still available in two or more of the new tbls list
            T = fields(aC)';
            tmp = ismember(obj.on, aC.(T{1}));
            for f = T(2:end)
                tmp = tmp + ismember(obj.on, aC.(f{:})); 
            end
            keep = tmp > 1;
            if any(~keep)
                obj.on = obj.on(keep);
                obj.errCode{end+1} = ['injoinQuery.setPropVals ALERT: table change required change in '...
                               'on property'];
            end
          % Clear where if necessary
            orphanedCols = setdiff(oldValidCols, obj.aCc);
            for c = orphanedCols
                if ~isempty(strfind(obj.where, c{:}))
                    obj.where = '';
                    obj.errCode{end+1} = ['injoinQuery.setPropVals ALERT: table change required '...
                                   'emptying of where property'];
                    break;
                end
            end
        end
        
        function wTbl = joinWTable(obj, arg)
            wTbl = arg;
            aC = obj.availableColumns;
            for t = obj.tbls
                if cellstrIsMember({arg}, aC.(t{:}))
                    wTbl = [t{:} '.' arg];
                    break;
                end
            end
        end
        
        function sC = get.selectClause(obj)
            sC = 'SELECT ';
            nc = numel(obj.cols);
            i = 1;
            while i < nc + 1

                word = obj.cols{i};

                if any(~cellfun(@isempty, regexp(word, obj.CSD.globalModifiers, 'once')))
                    sC = [sC word ' '];
                    i = i + 1;
                        
                elseif (i < nc) && any(~cellfun(@isempty, regexp(word, obj.CSD.singleColFuncs, 'once')))
                    sC = [sC word '(' obj.joinWTable(obj.cols{i+1}) '), '];
                    i = i + 2;

                elseif (i < nc - 2) && any(~cellfun(@isempty, regexp(word, obj.CSD.dateFuncs, 'once')))
                    sC = [sC word '(' obj.cols{i+1} ',' obj.joinWTable(obj.cols{i+2}) ',' ...
                          obj.joinWTable(obj.cols{i+3}) '), '];
                    i = i + 4;

                elseif (i < nc - 1) && strcmpi(obj.cols{i+1}, 'AS')
                    sC = [sC obj.joinWTable(word) ' AS ' obj.cols{i+2} ', '];                        
                    i = i + 3;
                    
                else
                    sC = [sC obj.joinWTable(word) ', '];                        
                    i = i + 1;
                end
                
            end
            
            if strcmp(sC(end-1), ',')
                sC = [sC(1:end-2) sC(end)];
            end
        end
                
        function fC = get.fromClause(obj)
          % Assembles the FROM clause 
          %
          % Tables to join are drawn, in order from obj.tbls.  obj.on must be a cellstr array of
          % either one column (shared by all the tables, which is checked by validateInput) or
          % precisely length(obj.tbls)-1 columns, where col(i) must be shared by obj.tbls(i+1) and
          % (at least) one of obj.tbls(1:i) (this too is checked by validateInput).
          %
          % Example: suppose obj.tbls = {'T1', 'T2', 'T3', 'T4'}
          %
          %   Allowed case 1: obj.on = {'C'}; assuming column C exists in each of tables T1-T4, 
          %     the resulting FROM clause is:
          %
          % FROM T1 INNER JOIN T2 ON T1.C=T2.C INNER JOIN T3 ON T3.C=T1.C INNER JOIN T4 ON T4.C=T1.C 
          %
          %   Allowed case 2: obj.on = {'C1', 'C2', 'C3'} (note: the C(i) need not be distinct); 
          %     assuming column C(i) exists in table T(i+1) and (at least) one of tables T(1:i),
          %     (for the sake of the example, assume it is T(i)) the resulting FROM clause is:
          %
          % FROM T1 INNER JOIN T2 ON T1.C1=T2.C1 INNER JOIN T3 ON T3.C2=T2.C2 INNER JOIN T4 ON T4.C3=T3.C3 
          %
          % Note: Due to the QAQC performed on inputs (as described above), the FROM clause here 
          %       assembled should work; however, it might produce redundant results.  Should this
          %       happen--and you don't want it to--add the word 'DISTINCT' at the beginning of the
          %       obj.cols list.
          
            global lfcr

            fC = ['FROM ' obj.tbls{1} ' INNER JOIN ' obj.tbls{2} ' ON '...            
                  obj.tbls{1} '.' obj.on{1} '=' obj.tbls{2} '.' obj.on{1} ' '];
            nt = numel(obj.tbls); no = numel(obj.on);
            if nt > 2 % joining more than 2 tables
                for i=3:nt
                    if no == 1
                        fC = [fC 'INNER JOIN ' obj.tbls{i} ' ON ' obj.tbls{i} '.' obj.on{1} ...
                              '=' obj.tbls{1} '.' obj.on{1} ' '];
                    else % more than one column on which to join
                      % Find a previous table that contains on{i-1}
                        for j=1:i-1
                            if cellstrIsMember(obj.on(i-1), obj.availableColumns.(obj.tbls{j}))
                                tbl = obj.tbls{j};
                                break;
                            end
                        end
                        if exist('tbl', 'var')
                            fC = [fC 'INNER JOIN ' obj.tbls{i} ' ON ' obj.tbls{i} '.' obj.on{i-1} ...
                              '=' tbl '.' obj.on{i-1} ' '];
                        else
                            obj.errCode{end+1} = ['injoinQuery.get.fromClause error: Something went '...
                                                  'horribly wrong (you should never see this error).'...
                                                  lfcr 'There must be some as yet undetected bug '...
                                                  'in that method''s "business logic".'];
                        end
                    end
                end
            end
        end
                
        function oC = get.orderbyClause(obj)
            oC = 'ORDER BY ';
            i = 1;
            oC = [oC sprintf('%s %s', obj.joinWTable(obj.orderBy{i}{1}), obj.orderBy{i}{2})];
            i = i + 1;
            while i < numel(obj.orderBy) + 1
                oC = [oC sprintf(', %s %s', obj.joinWTable(obj.orderBy{i}{1}), obj.orderBy{i}{2})];
                i = i + 1;
            end
            oC = [oC ';'];
            oC = regexprep(oC, {'asc', 'des'}, {'ASC', 'DESC'}, 'ignorecase');
        end
                
        function S = assembleStatement(obj)
            S = '';
            if ~isempty(obj.tbls)
                if isempty(obj.on)
                    obj.errCode{end+1} = ['injoinQuery.assembleStatement error: "on" property is '...
                                          'empty (need at least one column on which to join)'];
                else
                    S = [obj.selectClause obj.fromClause];
                end
            end
            if ~isempty(obj.where)
                S = [S ' WHERE ('];
                for i=1:(numel(obj.where)-1)
                    S = [S obj.where{i} ') OR ('];
                end
                S = [S obj.where{end} ')'];
            end
            if ~isempty(obj.orderBy)
                S = [S ' ' obj.orderbyClause];
            else
                S = [S ';'];
            end
        end

        function submitStatement(obj)
            tic;
                curs = exec(obj.con, obj.statement);
            obj.duration = toc;
            if isempty(curs.Message)
                curs = fetch(curs);
                if isempty(curs.Message)
                    obj.result = curs.Data;
                else
                    obj.errCode{end+1} = curs.Message;
                end
            else
                obj.errCode{end+1} = curs.Message;
            end
        end
        
    end
    
end